核心变更说明:本文基于原极客时间专栏"管理设计篇之分布式锁"一讲重写,全面更新至2026年技术栈。新增 Redis 7.x/Redisson 3.x、ZooKeeper 3.8、etcd v3.5 对比分析,深入剖析 Redlock 算法争议与替代方案,补充性能基准测试数据(来源:Redis.io 官方 Benchmark),提供完整的生产级代码示例。
在单机多线程环境下,我们使用 synchronized、ReentrantLock 或数据库行锁来保证共享资源的互斥访问。然而,当系统演变为分布式架构后,服务部署在多台机器上,甚至跨机房、跨可用区部署,传统的进程内锁机制完全失效。
| 场景 | 描述 | 并发风险 |
|---|---|---|
| 库存扣减 | 电商秒杀、商品库存扣减 | 超卖问题 |
| 金额操作 | 银行转账、账户余额更新 | 数据不一致 |
| 任务调度 | 定时任务防止重复执行 | 任务重复执行 |
| 幂等控制 | API 接口防重复提交 | 重复下单 |
| 配置变更 | 配置中心热更新 | 配置冲突 |
根据 Redis 官方文档 对分布式锁的定义,一个合格的分布式锁必须满足以下三个核心特性:
此外,生产环境还需要考虑:
| 特性 | Redis (Redisson) | ZooKeeper (Curator) | etcd (JetCD) | 数据库 |
|---|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ 极高 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 高 | ⭐⭐ 低 |
| 可靠性 | ⭐⭐⭐⭐ 高(需集群) | ⭐⭐⭐⭐⭐ 极高(ZAB协议) | ⭐⭐⭐⭐⭐ 极高(Raft) | ⭐⭐⭐ 中等 |
| 一致性 | 最终一致 | 强一致 | 强一致 | 强一致 |
| 实现复杂度 | 低(SDK成熟) | 中等 | 低 | 高 |
| 可重入支持 | ✅ 原生支持 | ✅ 支持 | ✅ 支持 | ❌ 需自行实现 |
| Watch/通知 | Pub/Sub | 原生 Watch | Watch | 轮询 |
| 适用场景 | 高并发、性能敏感 | 强一致、协调服务 | K8s原生、云原生 | 已有DB依赖 |
根据 Redis 官方文档,正确的加锁命令如下:
# 加锁:仅在 key 不存在时设置,并设置过期时间
SET resource_name my_random_value NX PX 30000参数说明:
NX:只在 key 不存在时才设置(Not eXist)PX:设置过期时间,单位毫秒(此处为30秒)my_random_value:全局唯一的标识符,用于安全释放锁解锁脚本(Lua):
-- 比对 value,确保只有锁的持有者才能释放
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end这是为了解决误释放问题。考虑以下场景:
解决方案:每个客户端生成唯一标识(如 UUID + 线程ID),释放锁时先比对 value。
Redis 作者 Antirez 在 Redlock 文章 中提出了 Redlock 算法,试图在 Redis 单节点故障时保证安全性。
Martin Kleppmann(剑桥大学分布式系统研究员)在 文章 中指出 Redlock 的根本缺陷:
问题核心:Redis 使用异步复制,在主节点宕机时可能发生锁状态丢失。即使使用 Redlock,也无法在 CAP 定理中同时满足一致性和可用性。
Martin 的论证(GC 暂停场景):
Antirez 的回应:承认 Redlock 不适合需要强一致性的场景(如金融交易),但在多数业务场景下是足够的。
Redisson 是 Java 生态中最成熟的 Redis 客户端,内置了完善的分布式锁实现。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.27.0</version>
</dependency>@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点模式
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("your_password")
.setDatabase(0)
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(24)
.setIdleConnectionTimeout(10000)
.setConnectTimeout(5000)
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}
// 集群模式(推荐生产使用)
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClusterClient() {
Config config = new Config();
config.useClusterServers()
// 集群节点列表
.addNodeAddress("redis://node1:6379",
"redis://node2:6379",
"redis://node3:6379")
.setPassword("your_password")
.setScanInterval(5000); // 集群状态扫描间隔
return Redisson.create(config);
}
}@Service
public class InventoryService {
@Autowired
private RedissonClient redissonClient;
/**
* 扣减库存 - 使用可重入锁
* @param 商品ID
* @param 扣减数量
* @return 是否成功
*/
public boolean deductStock(String productId, int quantity) {
// 定义锁的名称(建议包含业务标识)
String lockKey = "lock:inventory:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待 10 秒,锁定后 30 秒自动释放
// 注意:指定了 leaseTime 时 watchdog 机制不会启用(见下文说明)
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!acquired) {
log.warn("获取库存锁失败,商品ID: {}", productId);
return false;
}
// ====== 业务逻辑开始 ======
// 1. 查询库存
Integer currentStock = getStockFromDB(productId);
if (currentStock < quantity) {
log.info("库存不足,商品ID: {},当前库存: {}", productId, currentStock);
return false;
}
// 2. 扣减库存
boolean success = updateStockInDB(productId, currentStock - quantity);
if (success) {
log.info("扣减库存成功,商品ID: {},数量: {}", productId, quantity);
}
// ====== 业务逻辑结束 ======
return success;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("获取锁被中断", e);
return false;
} finally {
// 仅当锁是当前线程持有的才释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Redisson 的看门狗机制是其核心亮点:
关键点:
leaseTime 时才会启用 watchdogunlock() 后停止续期ZooKeeper 通过 ZAB(Zookeeper Atomic Broadcast)协议保证强一致性,其分布式锁实现基于临时顺序节点。
Apache Curator 是 ZooKeeper 的客户端框架,提供了高级别的锁API。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>@Service
public class ZkDistributedLockService {
private final InterProcessMutex lock;
public ZkDistributedLockService(CuratorFramework client) {
// 创建互斥锁,路径为 /locks/my-lock
this.lock = new InterProcessMutex(client, "/locks/my-lock");
}
public void doWithLock() {
try {
// 尝试获取锁,最多等待 10 秒
boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
if (acquired) {
try {
// 执行业务逻辑
executeBusinessLogic();
} finally {
lock.release();
}
} else {
log.warn("获取 ZooKeeper 锁超时");
}
} catch (Exception e) {
log.error("ZooKeeper 锁异常", e);
}
}
}| 维度 | ZooKeeper | Redis |
|---|---|---|
| 一致性模型 | CP(强一致) | AP(最终一致) |
| 性能(QPS) | ~10,000(受限于磁盘IO) | ~100,000+(纯内存) |
| 锁释放 | 会话断开自动释放 | 依赖 TTL / Watchdog |
| 实现复杂度 | 较高(需维护会话) | 较低(SDK成熟) |
| 适用场景 | 强一致需求(金融、支付) | 高并发、性能敏感 |
etcd 是 Cloud Native Computing Foundation (CNCF) 的毕业项目,采用 Raft 共识算法,是 Kubernetes 的数据存储后端。etcd v3.5+ 提供了成熟的并发原语。
// 使用 JetCD 客户端(推荐)
// Maven: io.etcd:jetcd-core:0.7.6
@Service
public class EtcdDistributedLockService {
private final Client etcdClient;
private final Lease leaseClient;
private final Lock lockClient;
public EtcdDistributedLockService() {
this.etcdClient = Client.builder()
.endpoints("http://etcd1:2379", "http://etcd2:2379")
.build();
this.leaseClient = etcdClient.getLeaseClient();
this.lockClient = etcdClient.getLockClient();
}
public void doWithLock(String lockName) {
try {
// 获取一个 30 秒的租约
LeaseGrantResponse leaseResp = leaseClient.grant(30).get();
long leaseId = leaseResp.getID();
// 使用租约创建锁
LockResponse lockResp = lockClient.lock(
ByteSequence.from(lockName, StandardCharsets.UTF_8),
leaseId
).get();
try {
// 业务逻辑
businessLogic();
} finally {
// 解锁
lockClient.unlock(lockResp.getKey()).get();
}
} catch (Exception e) {
log.error("etcd 锁操作异常", e);
}
}
}以下数据来源于各项目官方 Benchmark 和社区测试报告:
| 方案 | 加锁 QPS | 解锁 QPS | 平均延迟 | P99 延迟 | 测试环境 |
|---|---|---|---|---|---|
| Redis (单节点) | ~120,000 | ~120,000 | 0.5ms | 2ms | Redis 7.2, 8核32G |
| Redis (Cluster) | ~400,000 | ~400,000 | 0.8ms | 5ms | 3主3从, 同上 |
| Redisson RLock | ~95,000 | ~95,000 | 1.0ms | 8ms | 含 watchdog 开销 |
| ZooKeeper | ~12,000 | ~12,000 | 5ms | 25ms | ZK 3.8, 8核16G |
| etcd v3.5 | ~18,000 | ~18,000 | 3ms | 15ms | 3节点集群 |
| MySQL (FOR UPDATE) | ~3,000 | ~3,000 | 15ms | 80ms | MySQL 8.0, SSD |
数据来源:Redis Benchmark、ZooKeeper Wiki、etcd Benchmark
图例:蓝色=Redis,橙色=etcd,绿色=ZooKeeper
/**
* 库存扣减服务 - 生产级实现
*
* 设计要点:
* 1. 使用 Redisson 可重入锁
* 2. 双层校验(缓存 + 数据库 CAS)
* 3. 异步扣减,提升吞吐
* 4. 完善的日志与监控
*/
@Service
@Slf4j
public class StockDeductionServiceImpl implements StockDeductionService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ProductMapper productMapper;
@Autowired
private ApplicationEventPublisher eventPublisher;
/** 锁等待时间(秒) */
private static final long LOCK_WAIT_TIME = 5;
/** 锁持有时间(秒) */
private static final long LOCK_LEASE_TIME = 10;
/** 库存缓存 Key 前缀 */
private static final String STOCK_CACHE_PREFIX = "stock:";
@Override
public StockDeductResult deduct(String productId, int quantity, String orderId) {
long startTime = System.currentTimeMillis();
// 1. 快速路径:检查缓存中的库存是否充足
String cacheKey = STOCK_CACHE_PREFIX + productId;
String stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
if (stockStr != null) {
int cachedStock = Integer.parseInt(stockStr);
if (cachedStock < quantity) {
return StockDeductResult.fail("库存不足", cachedStock);
}
}
// 2. 获取分布式锁
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(
LOCK_WAIT_TIME,
LOCK_LEASE_TIME,
TimeUnit.SECONDS
);
if (!locked) {
log.warn("获取库存锁失败,productId: {}, orderId: {}", productId, orderId);
return StockDeductResult.fail("系统繁忙,请稍后重试", -1);
}
// 3. 二次校验库存(双重检查)
stockStr = stringRedisTemplate.opsForValue().get(cacheKey);
int currentStock = (stockStr != null) ? Integer.parseInt(stockStr)
: getStockFromDB(productId);
if (currentStock < quantity) {
// 刷新缓存
refreshStockCache(productId, currentStock);
return StockDeductResult.fail("库存不足", currentStock);
}
// 4. 数据库 CAS 更新(乐观锁)
int affected = productMapper.decreaseStockWithCAS(productId, quantity, currentStock);
if (affected > 0) {
// 5. 更新缓存
int newStock = currentStock - quantity;
stringRedisTemplate.opsForValue().set(cacheKey, String.valueOf(newStock), 1, TimeUnit.HOURS);
// 6. 发布事件(异步处理后续逻辑)
eventPublisher.publishEvent(new StockDeductedEvent(productId, quantity, orderId));
long cost = System.currentTimeMillis() - startTime;
log.info("库存扣减成功,productId: {}, orderId: {}, 耗时: {}ms", productId, orderId, cost);
return StockDeductResult.success(newStock);
} else {
// CAS 失败,说明有并发修改
log.warn("库存扣减CAS失败,productId: {}", productId);
return StockDeductResult.fail("库存变化,请重试", -1);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return StockDeductResult.fail("操作中断", -1);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 从数据库查询库存(带兜底)
*/
private int getStockFromDB(String productId) {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new BusinessException("商品不存在");
}
return product.getStock();
}
/**
* 刷新库存缓存
*/
private void refreshStockCache(String productId, int stock) {
stringRedisTemplate.opsForValue().set(
STOCK_CACHE_PREFIX + productId,
String.valueOf(stock),
1, TimeUnit.HOURS
);
}
}@Mapper
public interface ProductMapper {
/**
* 使用乐观锁扣减库存
* UPDATE ... SET stock = stock - #{quantity} WHERE id = #{id} AND stock = #{expectedStock}
*/
@Update("UPDATE product SET stock = stock - #{quantity}, " +
"update_time = NOW() " +
"WHERE id = #{productId} AND stock >= #{quantity} AND stock = #{expectedStock}")
int decreaseStockWithCAS(@Param("productId") String productId,
@Param("quantity") int quantity,
@Param("expectedStock") int expectedStock);
}lock:order:{userId}:{orderId}),避免锁范围过大| 陷阱 | 说明 | 规避方法 |
|---|---|---|
| 锁未释放 | 忘记在 finally 中 unlock | 使用 try-with-resources 或标准模板 |
| 锁时间太短 | 业务未完成锁就过期了 | 使用 watchdog 或合理估算时间 |
| 锁时间太长 | 故障时长时间不释放 | 设置合理的超时时间 |
| Redis 主从切换丢锁 | 异步复制导致锁丢失 | 使用 Redlock 或接受风险 |
| GC 导致的时钟跳跃 | STW 导致锁提前过期 | 使用 ZooKeeper/etcd 或增大缓冲 |
| 网络分区 | 脑裂情况下的安全问题 | 采用多数派决策(Paxos/Raft) |
本文版本:2026 重制版 | 基于原极客时间专栏"管理设计篇之分布式锁"一讲重构 最后更新:2026-06-06 | 技术栈:Redis 7.2 / Redisson 3.27 / ZooKeeper 3.8 / etcd 3.5